廢話不多說,直接來看範例,先來優化忘記密碼送出 email 後的接收頁
controller/do_forget.php
<?php
//if form has been submitted process it
if(isset($_POST['submit']) AND isset($_POST['email']))
{
$email = $_POST['email'];
$postVeridator = new PostVeridator();
$userVeridator = new UserVeridator();
$userAction = new UserAction();
$log = new Log();
if($postVeridator->isValidEmail($email)) { // 信箱是否合法
if($userVeridator->isEmailDuplicate($email)) { // 信箱是否存在
try {
$resetToken = $userAction->getResetToken(); // 創建 Token 並存到資料庫
$userAction->sendResetEmail($resetToken); // 用 Token 組出重置信件並寄出
$userAction->redir2login(); // 重導向登入頁並顯示成功
} catch(PDOException $e) {
$error[] = $e->getMessage();
$log->error(__FILE__, json_encode($error));
}
}else{ // 不存在就假裝成功, 避免被試出會員信箱
$log->warning(__FILE__, 'WRONG EMAIL: ' .$email);
sleep(rand(1,2));
$userAction->redir2login(); // 重導向登入頁並顯示成功
exit;
}
} else { // 不合法就顯示踢回上一頁顯示錯誤
header('Location: ' . $_SERVER['HTTP_REFERER']);
exit;
}
}else{ // 非正常進入就踢回首頁
header('Location: ' . Config::BASE_URL);
exit;
}
所需要新材料有:
veridators/PostVeridator.php
<?php
class PostVeridator {
public function isValidEmail( $email )
{
$data_array['email'] = $email;
$gump = new GUMP();
$data_array = $gump->sanitize($data_array);
$validation_rules_array = array(
'email' => 'required|valid_email'
);
$gump->validation_rules($validation_rules_array);
$filter_rules_array = array(
'email' => 'trim|sanitize_email'
);
$gump->filter_rules($filter_rules_array);
$validated_data = $gump->run($data_array);
if($validated_data === false) {
$error = $gump->get_readable_errors(false);
$msg = new \Plasticbrain\FlashMessages\FlashMessages();
foreach( $error as $e) {
$msg->error($e);
}
return false;
} else {
return true;
}
}
}
veridators/UserVeridator.php
<?php
/**
* 耦合使用 Database 物件進行資料庫驗證 username 與 email 是否已存在於資料庫
*/
class UserVeridator {
private $error;
/**
* 驗證是否已登入
*/
public static function isLogin($username){
if($username != ''){
return true;
} else{
return false;
}
}
/**
* 可取出錯誤訊息字串陣列
*/
public function getErrorArray(){
return $this->error;
}
/**
* 驗證二次密碼輸入是否相符
*/
public function isPasswordMatch($password, $passwrodConfirm){
if ($password != $passwrodConfirm){
$this->error[] = 'Passwords do not match.';
return false;
}
return true;
}
/**
* 驗證帳號密碼是否正確可登入
*/
public function loginVerification($username, $password){
$result = Database::get()->execute('SELECT * FROM members WHERE active = "Yes" AND username = :username', array(':username' => $username));
if(isset($result[0]['memberID']) and !empty($result[0]['memberID'])) {
$passwordObject = new Password();
if($passwordObject->password_verify($password,$result[0]['password'])){
return true;
}
}
$this->error[] = 'Wrong username or password or your account has not been activated.';
return false;
}
/**
* 驗證帳號是否已存在於資料庫中
*/
public function isUsernameDuplicate($username){
$result = Database::get()->execute('SELECT username FROM members WHERE username = :username', array(':username' => $username));
if(isset($result[0]['username']) and !empty($result[0]['username'])){
$this->error[] = 'Username provided is already in use.';
return false;
}
return true;
}
/**
* 驗證此帳號 ID 跟 開通碼 hash 是否已存在於資料庫中
*/
public function isReady2Active($id, $active){
$result = Database::get()->execute('SELECT username FROM members WHERE memberID = :memberID AND active = :active', array(':memberID' => $id, ':active' => $active));
if(isset($result[0]['username']) and !empty($result[0]['username'])){
return true;
}else{
$this->error[] = 'Username provided is already in use.';
return false;
}
}
/**
* 驗證信箱是否已存在於資料庫中
*/
public function isEmailDuplicate($email){
$result = Database::get()->execute('SELECT email FROM members WHERE email = :email', array(':email' => $email));
if(isset($result[0]['email']) AND !empty($result[0]['email'])){
$this->error[] = 'Email provided is already in use.';
return true;
}
return false;
}
}
libraries/Ip.php 專門取得網站用戶的 IP 已便存到 LOG
<?php
class Ip {
static function get(){
if (!empty($_SERVER["HTTP_CLIENT_IP"])) {
$ip = $_SERVER["HTTP_CLIENT_IP"];
} elseif (!empty($_SERVER["HTTP_X_FORWARDED_FOR"])) {
$ip = $_SERVER["HTTP_X_FORWARDED_FOR"];
} else {
$ip = $_SERVER["REMOTE_ADDR"];
}
return $ip;
}
}
action/User.php 幫助我們對使用者操作封裝
<?php
class UserAction {
function getResetToken(){
$data_array['resetComplete'] = 'No';
$data_array['resetToken'] = md5(rand().time());
Database::get()->update('members', $data_array, "memberID", $memberID);
$resetToken = $data_array['resetToken'];
}
function sendResetEmail($resetToken){
$body = "<p>Someone requested that the password be reset.</p>
<p>If this was a mistake, just ignore this email and nothing will happen.</p>
<p>To reset your password, visit the following address: <a href='".Config::BASE_URL."reset/$resetToken'>".Config::BASE_URL."reset/$resetToken</a></p>";
$mail = new Mail(Config::MAIL_USER_NAME, Config::MAIL_USER_PASSWROD);
$mail->setFrom(Config::MAIL_FROM, Config::MAIL_FROM_NAME);
$mail->addAddress($email);
$mail->subject("Password Reset");
$mail->body($body);
$mail->send();
}
function redir2login(){
$msg = new \Plasticbrain\FlashMessages\FlashMessages();
$msg->success("Please check your inbox for a reset link.");
header('Location: '.Config::BASE_URL.'login');
exit;
}
}
libraries/Log.php
<?php
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use Monolog\Handler\FirePHPHandler;
class Log {
function error($page, $msg){
$logger = new Logger($page);
$logger->pushHandler(new StreamHandler('log/error.log', Logger::DEBUG));
$logger->pushHandler(new FirePHPHandler());
$logger->error(Ip::get().': '.$msg);
}
function info($page, $msg){
$logger = new Logger($page);
$logger->pushHandler(new StreamHandler('log/info.log', Logger::DEBUG));
$logger->pushHandler(new FirePHPHandler());
$logger->info(Ip::get().': '.$msg);
}
function warning($page, $msg){
$logger = new Logger($page);
$logger->pushHandler(new StreamHandler('log/warning.log', Logger::DEBUG));
$logger->pushHandler(new FirePHPHandler());
$logger->warning(Ip::get().': '.$msg);
}
}
我們創了一個更容易使用 monolog 的 Log class 物件
在使用上只需要
$log = new Log();
$log->error(__FILE__, 'error msg put here');
另外也創了一個資料夾專門放 log 裡面有三個檔案
log/error.log
log/warning.log
log/info.log
記得要對 log 資料夾與其底下所有檔案變更權限成可讀可寫,指令是:
chmod -R 777 log
這樣一來,我們就可以在發生問題時快速追蹤 log 找到錯誤,或是抓出想亂嘗試 email 的 IP 位置,防患未然,以免發生狀況時完全沒有 log 紀錄那真的蠻悲劇的...
仔細確認後才發現大大的 userVeridator 的方法中,幾個確認資料庫數值重複的部分的回傳值跟舊版本的完全相反了,所以userVeridator如果不跟著修改,forget是不會寄信出去,因為判定部分剛好相反,所以打了正確的mail會被當作不存在而收不到信件
class UserAction的方法有少寫一些東西,會導致無法寄出信件
function getResetToken($email){
$result = Database::get()->execute('SELECT memberID FROM members WHERE email = :email', array(':email' => $email));
$memberID = $result[0]['memberID'];
$data_array['resetComplete'] = 'No';
$data_array['resetToken'] = md5(rand().time());
Database::get()->update('members', $data_array, "memberID", $memberID);
$resetToken = $data_array['resetToken'];
return $resetToken;
}
這樣可以取得memberID,並且回傳resetToken回去
而 function sendResetEmail($resetToken,$email){...}
兩個方法都須加上$email才能使用,不然會因為沒有$email而不知傳送到哪(在do_forget中因為下一行是直接跳轉到login網頁,因此報錯訊息沒看到就直接轉頁面了)
在 do_forget.php
因應上面方法的修改,要跟著改成這樣,把參數加上去
$resetToken = $userAction->getResetToken($email); // 創建 Token 並存到資料庫
$userAction->sendResetEmail($resetToken,$email); // 用 Token 組出重置信件並寄出